package sindice.service.blackbook;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import sindice.util.SindiceUtils;

import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.vocabulary.OWL;

import blackbook.service.api.BlackbookServiceBrokerException;
import blackbook.service.api.ConfigurableAlgorithmParameter;
import blackbook.service.api.EdgeDecorator;
import blackbook.service.api.ResourceDecorator;
import blackbook.service.api.ResourceDetails;

/**
 * A service broker is a convenience object for interfacing with a Blackbook
 * instance using Blackbook services. The broker takes care of the service
 * interaction and converts information to a more convenient representation.
 * 
 */
public class BlackbookServiceBrokerSindice implements
		BlackbookServiceSindiceInterface {

	/** logger */
	private static Log logger = LogFactory
			.getLog(BlackbookServiceBrokerSindice.class);

	public static final long URL_READ_TIMEOUT = 5000;
	
	private long urlReadTimeout;

	public static final int SINDICE_TOTAL_RESULTS_DESIRED = 25;
	
	private int maxResults;

	public static final String[] SUPPORTED_RDF_FORMATS = { "RDF" };

	private boolean graphChanged = true;

	Set<String> uris = new HashSet<String>();
	Set<String> selectedURIs = new HashSet<String>();
	List<ConfigurableAlgorithmParameter> algorithmParameters = new ArrayList<ConfigurableAlgorithmParameter>();
	Map<String, ResourceDecorator> resourceDecorators = new HashMap<String, ResourceDecorator>();
	Map<String, ResourceDetails> resourceDetails = new HashMap<String, ResourceDetails>();
	String tempDSName = "";
	Model test = ModelFactory.createDefaultModel();
	String baseURL = "";
	String publicKey = "";
	private static final int MAX_EXPAND = 1000;

	// Set<String> initialURIs = new HashSet<String>();

	public BlackbookServiceBrokerSindice() {
		setMaxResults(SINDICE_TOTAL_RESULTS_DESIRED);
		setUrlReadTimeout(URL_READ_TIMEOUT);
	}
	
	public BlackbookServiceBrokerSindice(int maxResults,long urlReadTimeout) {
		setMaxResults(maxResults);
		setUrlReadTimeout(urlReadTimeout);
	}

	/**
	 * @see blackbook.service.api.BlackbookServiceBrokerIfc#deleteSelectedNodes()
	 */
	public void deleteSelectedNodes() {
		// TODO Auto-generated method stub
		logger.info("deleting selected nodes: " + selectedURIs);
		for (String uri : selectedURIs) {
			uris.remove(uri);
			resourceDecorators.remove(uri);
			resourceDetails.remove(uri);
		}

		selectedURIs = new HashSet<String>();
		graphChanged = true;
	}

	/**
	 * @see blackbook.service.api.BlackbookServiceBrokerIfc#deselectNodes()
	 */
	public void deselectNodes() throws BlackbookServiceBrokerException {
		logger.info("deselecting all nodes");
		for (String uri : selectedURIs) {
			resourceDecorators.get(uri).setSelected(false);
		}
		selectedURIs = new HashSet<String>();
		graphChanged = true;
	}

	/**
	 * @see blackbook.service.api.BlackbookServiceBrokerIfc#deselectNodes(java.util.Set)
	 */
	public void deselectNodes(Set<String> uris)
			throws BlackbookServiceBrokerException {
		// TODO Auto-generated method stub
		for (String uri : uris) {
			logger.info("dselecting nodes: " + uri);
			resourceDecorators.get(uri).setSelected(false);
			selectedURIs.remove(uri);
		}
		graphChanged = true;
	}

	@Override
	public Set<String> dipSelectedNodes()
			throws BlackbookServiceBrokerException {
		// no op right now
		logger.info("dip is currently unimplemented");
		graphChanged = true;
		return new HashSet<String>();
	}

	@Override
	public Set<String> expandSelectedNodes()
			throws BlackbookServiceBrokerException {
		Set<String> expandURIs = new HashSet<String>();
		logger.info("expanding selected nodes, but only up to " + MAX_EXPAND
				+ " new nodes");
		for (String sURI : selectedURIs) {
			StmtIterator stmts = test.listStatements(test.createResource(sURI),
					(Property) null, (RDFNode) null);
			while (stmts.hasNext()) {
				Statement stmt = stmts.nextStatement();
				if (stmt.getObject().isURIResource()
						&& !uris.contains(stmt.getObject().isURIResource())) {
					expandURIs.add(stmt.getObject().asNode().getURI());
				} else {
					// not interested
				}
			}
		}

		logger.info("a total of " + expandURIs.size() + " found.");

		if (expandURIs.size() > MAX_EXPAND) {
			logger.info("cutting the list down to " + MAX_EXPAND);
			HashSet<String> fullList = new HashSet<String>();
			fullList.addAll(expandURIs);
			expandURIs.clear();
			Iterator<String> sItr = fullList.iterator();
			for (int i = 0; i < MAX_EXPAND && sItr.hasNext(); i++) {
				String curURI = sItr.next();
				if (uris.contains(curURI)) {
					logger
							.info("      skipping expanded uri, we already have it "
									+ curURI);
					i--;
				} else {
					logger.info("     adding expanded uri " + curURI);
					expandURIs.add(curURI);
				}
			}
		}

		// need to do this separately from iterating over the statements in
		// order to avoid concurrent modification exception
		for (String eURI : expandURIs) {
			try {
				if (SindiceUtils.isTimelyRdfUrl(eURI, getUrlReadTimeout())) {
					logger.info("expand is adding statements from " + eURI);
					test.add(SindiceUtils.readRdfUrl(eURI, getUrlReadTimeout(),
							false));
					uris.add(eURI);
				}
			} catch (Exception e) {
				logger.error("while expanding, skipping the expansion of uri: "
						+ eURI);
			}
		}

		resourceDecorators = loadResourceDecorators(uris, test, selectedURIs);
		resourceDetails = loadResourceDetails(resourceDecorators);
		graphChanged = true;
		return expandURIs;
	}

	@Override
	public List<ConfigurableAlgorithmParameter> getAlgorithmParameters(
			String algorithm) throws BlackbookServiceBrokerException {
		List<ConfigurableAlgorithmParameter> algList = new ArrayList<ConfigurableAlgorithmParameter>();
		logger.info("getting algorithm parameters");
		for (ConfigurableAlgorithmParameter algParam : this.algorithmParameters) {
			if (algParam.getAlgorithm().equalsIgnoreCase(algorithm)) {
				algList.add(algParam);
			}
		}

		return algList;
	}

	@Override
	public List<String> getAlgorithms() throws BlackbookServiceBrokerException {
		List<String> algList = new ArrayList<String>();
		logger.info("getting algorithms");
		for (ConfigurableAlgorithmParameter algParam : this.algorithmParameters) {
			if (!algList.contains(algParam.getAlgorithm())) {
				algList.add(algParam.getAlgorithm());
			}
		}

		return algList;
	}

	@Override
	public Map<String, ResourceDecorator> getResourceDecorators(Set<String> uris)
			throws BlackbookServiceBrokerException {
		Map<String, ResourceDecorator> resMap = new HashMap<String, ResourceDecorator>();
		for (String uri : uris) {
			if (resourceDecorators.get(uri) != null)
				resMap.put(uri, resourceDecorators.get(uri));
		}

		return resMap;
	}

	@Override
	public Map<String, ResourceDetails> getResourceDetails(Set<String> uris)
			throws BlackbookServiceBrokerException {
		Map<String, ResourceDetails> resMap = new HashMap<String, ResourceDetails>();
		logger.info("getting resource details");
		for (String uri : uris) {
			if (resourceDetails.get(uri) != null)
				resMap.put(uri, resourceDetails.get(uri));
		}

		return resMap;
	}

	@Override
	public Set<String> getSelectedURIs() throws BlackbookServiceBrokerException {
		logger.info("getting selected uris: " + selectedURIs.size());
		return selectedURIs;
	}

	@Override
	public Set<String> getURIs() throws BlackbookServiceBrokerException {
		logger.info("getting URIs" + uris.size());
		return uris;
	}

	@Override
	public boolean isAlgorithmConfigurable(String algorithm)
			throws BlackbookServiceBrokerException {
		return true;
	}

	@Override
	public Set<String> query(String query)
			throws BlackbookServiceBrokerException {
		logger.info("running query: " + query);
		deselectNodes();
		uris.clear();

		Set<String> sURIs = SindiceUtils.sindiceQuery(query,
				getMaxResults(), SUPPORTED_RDF_FORMATS);
		uris.addAll(sURIs);
		test = createModelFromURIs(uris);
		uris.removeAll(SindiceUtils.getBannedURLs().keySet());
		resourceDecorators = loadResourceDecorators(uris, test, selectedURIs);
		resourceDetails = loadResourceDetails(resourceDecorators);
		graphChanged = true;
		return uris;
	}

	@Override
	public void selectAllNodes() throws BlackbookServiceBrokerException {
		selectedURIs = new HashSet<String>();
		selectedURIs.addAll(uris);
		for (String uri : selectedURIs) {
			resourceDecorators.get(uri).setSelected(true);
		}
		logger.info("selecting all nodes " + uris.size());
		graphChanged = true;
	}

	@Override
	public void selectNodes(Set<String> uris)
			throws BlackbookServiceBrokerException {
		selectedURIs = new HashSet<String>();
		logger.info("selecting nodes: " + uris);
		selectedURIs.addAll(uris);
		for (String uri : uris) {
			resourceDecorators.get(uri).setSelected(true);
		}
		graphChanged = true;
	}

	public String setTempDSName(String tempDS)
			throws BlackbookServiceBrokerException {
		tempDSName = tempDS;
		return tempDSName;

	}

	public static List<ResourceDetails> produceDetails(Resource res,
			Model model, Set<Resource> breadCrumbs) {
		List<ResourceDetails> rDets = new ArrayList<ResourceDetails>();

		try {
			// if already processed, don't process
			if (breadCrumbs.contains(res)) {
				return rDets;
			}
			// don't process after this processing
			breadCrumbs.add(res);

			ResourceDetails current = new ResourceDetails();
			StmtIterator stmts = model.listStatements(res, (Property) null,
					(RDFNode) null);
			while (stmts.hasNext()) {
				Statement stmt = stmts.nextStatement();
				if (stmt.getObject().isLiteral()) {
					current = new ResourceDetails();
					current.setLabel(stmt.getObject().asNode().getLiteral()
							.toString());
				} else if (stmt.getObject().isAnon()) {
					List<ResourceDetails> children = current.getChildren();
					children.addAll(produceDetails(model.createResource(stmt
							.getObject().asNode().getBlankNodeId()), model,
							breadCrumbs));
					current.setChildren(children);
				} else {
					// not interested
				}

				rDets.add(current);
			}
		} catch (Exception e) {
			logger.error("error while producing details");
			e.printStackTrace();
		}
		return rDets;

	}

	public static Set<EdgeDecorator> produceEdgeDecorators(Resource res,
			Model model) {
		Set<EdgeDecorator> eDecs = new HashSet<EdgeDecorator>();

		EdgeDecorator current = new EdgeDecorator();
		StmtIterator stmts = model.listStatements(res, (Property) null,
				(RDFNode) null);
		while (stmts.hasNext()) {
			Statement stmt = stmts.nextStatement();
			if (stmt.getObject().isURIResource()) {
				current = new EdgeDecorator();
				current.setFromURI(res.getURI());
				current.setToURI(stmt.getObject().asNode().getURI());
				current.setColor("309699");
				current.setRoles(new HashSet<String>());
				current.setSubLabels(new ArrayList<String>());
				current.setLabel(stmt.getPredicate().getURI());
			} else {
				// not interested
			}

			eDecs.add(current);
		}

		return eDecs;

	}

	// private static Set<String> loadURIsFromModel(Model model) {
	// // uris and selected uris
	// StmtIterator stmts = model.listStatements();
	// Set<String> uris = new HashSet<String>();
	//
	// while (stmts.hasNext()) {
	// Statement stmt = stmts.nextStatement();
	// try {
	// if (stmt.getResource().isURIResource()) {
	// uris.add(stmt.getResource().getURI());
	// logger.info("added uri " + stmt.getResource().getURI()
	// + " from model.");
	// }
	// } catch (Exception e) {
	// logger.error("error while loading uris from model");
	// // e.printStackTrace();
	// }
	// }
	//
	// return uris;
	// }

	private Model createModelFromURIs(Set<String> uris) {
		Model model = ModelFactory.createDefaultModel();
		HashSet<String> loadURIs = new HashSet<String>();
		loadURIs.addAll(uris);
		for (String uri : loadURIs) {
			try {
				model
						.add(SindiceUtils.readRdfUrl(uri, getUrlReadTimeout(),
								false));
			} catch (Exception e) {
				logger.error("cannot read rdf from uri " + uri
						+ ".  it will be banned.");
				uris.remove(uri);
				resourceDecorators.remove(uri);
				resourceDetails.remove(uri);
			}
		}

		return model;
	}

	private static Map<String, ResourceDecorator> loadResourceDecorators(
			Set<String> uris, Model model, Set<String> selectedURIs) {

		Map<String, ResourceDecorator> rDecs = new HashMap<String, ResourceDecorator>();

		try {

			int count = 0;
			for (String uri : uris) {
				// resource decorator
				ResourceDecorator rDec = new ResourceDecorator();
				rDec.setDatasourceColor("" + count % 10 + count % 10 + count
						% 10 + count % 10 + count % 10 + count % 10);
				rDec.setDate(new Date(System.currentTimeMillis()));
				rDec.setDescription("description #" + count);
				ResourceDetails rDets = new ResourceDetails();
				rDets.setChildren(produceDetails(model.createResource(uri),
						model, new HashSet<Resource>()));
				rDec.setDetails(rDets);
				rDec.setDetailsRendered(false);
				Set<EdgeDecorator> edgeDecorators = produceEdgeDecorators(model
						.createResource(uri), model);
				rDec.setEdgeDecorators(edgeDecorators);
				if (model.listObjectsOfProperty(model.createResource(uri),
						model.createProperty("http://xmlns.com/foaf/0.1/img"))
						.hasNext()) {
					String iconURI = model
							.listObjectsOfProperty(
									model.createResource(uri),
									model
											.createProperty("http://xmlns.com/foaf/0.1/img"))
							.nextNode().asNode().getURI();
					rDec.setIcon(iconURI);
				} else {
					rDec
							.setIcon("http://wiki.jcptest.info/themes/icon_styles/tango/large/face-smile.png");
				}
				if (model
						.listObjectsOfProperty(
								model.createResource(uri),
								model
										.createProperty("http://www.w3.org/2000/01/rdf-schema#label"))
						.hasNext()) {
					rDec
							.setLabel(model
									.listObjectsOfProperty(
											model.createResource(uri),
											model
													.createProperty("http://www.w3.org/2000/01/rdf-schema#label"))
									.nextNode().asNode().getLiteral()
									.toString());
				} else {
					rDec.setLabel(uri);
				}

				rDec.setSelected(selectedURIs.contains(uri));

				if (model
						.listObjectsOfProperty(
								model.createResource(uri),
								model
										.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"))
						.hasNext()) {
					rDec
							.setType(model
									.listObjectsOfProperty(
											model.createResource(uri),
											model
													.createProperty("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"))
									.nextNode().asNode().getURI());
				} else {
					rDec.setType(OWL.Thing.getURI());
				}
				rDec.setUri(uri);
				rDecs.put(uri, rDec);

			}

		} catch (Exception e) {
			logger
					.error("error while loading resource decorators in BlackbookServiceBrokerSoapWanabe");
			e.printStackTrace();
		}
		return rDecs;
	}

	private static Map<String, ResourceDetails> loadResourceDetails(
			Map<String, ResourceDecorator> rDecs) {
		Map<String, ResourceDetails> rDets = new HashMap<String, ResourceDetails>();
		for (Entry<String, ResourceDecorator> entry : rDecs.entrySet()) {
			rDets.put(entry.getKey(), entry.getValue().getDetails());
		}

		return rDets;
	}

	@Override
	public void setConfigurationParameters(String tempDS, String baseUrl,
			String publicKey) throws BlackbookServiceBrokerException {
		this.tempDSName = tempDS;
		this.baseURL = baseUrl;
		this.publicKey = publicKey;

	}

	public boolean hasGraphChanged() {
		if (graphChanged) {
			graphChanged = false;
			return true;
		}

		return false;
	}
	
	public long getUrlReadTimeout() {
		return urlReadTimeout;
	}

	public void setUrlReadTimeout(long urlReadTimeout) {
		this.urlReadTimeout = urlReadTimeout;
	}
	
	public int getMaxResults() {
		return maxResults;
	}

	public void setMaxResults(int maxResults) {
		this.maxResults = maxResults;
	}

}